#include <gtest/gtest.h>

#include <variant>

#include "JSIExecutorTest.h"
#include "test_native/MyNativeModule.h"
#include "utils/Dynamic.h"

Dynamic testCalc(int n, int max) {
  logInfo("testCalc called with param: n: " + std::to_string(n) + ", max: " + std::to_string(max) + "\n");
  int sum = 0;
  for (int i = 1; i < n; i++) {
    sum += i;
  }
  auto ret = sum < max ? sum : max;
  logInfo("testCalc returned: " + std::to_string(ret) + "\n");
  return Dynamic((int64_t)ret);
}

TEST_P(JSIExecutorTest, Invoke) {
  testing::internal::CaptureStdout();
  std::vector<std::unique_ptr<jsiexecutor::NativeModule>> nativeModules;
  auto myNativeModule = std::make_unique<test_native_module::MyNativeModule>(1);
  myNativeModule->addFunc("testCalc", testCalc, jsiexecutor::MethodType::SYNC);
  nativeModules.emplace_back(std::move(myNativeModule));

  jsiexecutor::JSIExecutor jsiExecutor(std::move(runtime),
                                           std::make_shared<jsiexecutor::ModuleRegistry>(std::move(nativeModules)));
  jsiExecutor.initializeRuntime();
  auto params = facebook::jsi::Array::createWithElements(*jsiExecutor.getRuntime(),
                                                         {facebook::jsi::Value((int)3), facebook::jsi::Value((int)15)});
  auto ret = jsiExecutor.callNativeModule("myNativeModule", "testCalc", std::move(params));
  logInfo("calling myNativeModule + testCalc with params [3, 15] expecting result: " +
          std::to_string((int64_t)ret.getNumber()) + "\n");

  params = facebook::jsi::Array::createWithElements(*jsiExecutor.getRuntime(),
                                                    {facebook::jsi::Value((int)1), facebook::jsi::Value((int)2)});
  ret = jsiExecutor.callNativeModule("myNativeModule", "testFunc", std::move(params));
  logInfo("calling myNativeModule + testFunc with params [1, 2] expecting null: " + std::to_string(ret.isNull()) +
          "\n");

  std::string script = R"(
      console.log("test native module");
      var ret = globalThis.invoke("myNativeModule", "testCalc", [3, 15]);
      console.log("invoke called and returned: " + ret);

      ret = globalThis.invoke("myNativeModule", "testFunc", [1, 2]);
      console.log("invoke called and returned: " + ret);
    )";
  jsiExecutor.loadBundle(std::make_unique<facebook::jsi::StringBuffer>(script), "test");
  std::string output = testing::internal::GetCapturedStdout();

  EXPECT_EQ(countOccurrences(output, "testCalc called with param: n: 3, max: 15"), 2);
  EXPECT_EQ(countOccurrences(output, "testCalc returned: 3"), 2);
  EXPECT_NE(output.find("calling myNativeModule + testCalc with params [3, 15] expecting result: 3"),
            std::string::npos);

  EXPECT_EQ(countOccurrences(output, "testFunc called with param x: 1, y:2"), 2);
  EXPECT_NE(output.find("calling myNativeModule + testFunc with params [1, 2] expecting null: 1"), std::string::npos);

  EXPECT_EQ(countOccurrences(output, "calling invoke in MyNativeModule method"), 4);
}

TEST_P(JSIExecutorTest, NativeModule) {
  testing::internal::CaptureStdout();
  std::vector<std::unique_ptr<jsiexecutor::NativeModule>> nativeModules;
  auto myNativeModule = std::make_unique<test_native_module::MyNativeModule>(1);
  myNativeModule->addFunc("testCalc", testCalc, jsiexecutor::MethodType::SYNC);
  nativeModules.emplace_back(std::move(myNativeModule));

  jsiexecutor::JSIExecutor jsiExecutor(std::move(runtime),
                                           std::make_shared<jsiexecutor::ModuleRegistry>(std::move(nativeModules)));
  jsiExecutor.initializeRuntime();
  std::string script = R"(
    const NativeModules = globalThis.NativeModules;
    ret = NativeModules["myNativeModule"]["testCalc"](3, 5);
    console.log("invoke called and returned: " + ret);

    const { myNativeModule } = NativeModules;
    ret = myNativeModule["testFunc"](1, 2);
    console.log("invoke called and returned: " + ret);

    console.log("myNativeModule has constant x: " + myNativeModule.x + ", constant y: " + myNativeModule.y);
  )";
  jsiExecutor.loadBundle(std::make_unique<facebook::jsi::StringBuffer>(script), "test");
  std::string output = testing::internal::GetCapturedStdout();

  EXPECT_EQ(countOccurrences(output, "calling invoke in MyNativeModule method"), 2);

  EXPECT_NE(output.find("testCalc called with param: n: 3, max: 5"), std::string::npos);
  EXPECT_NE(output.find("testCalc returned: 3"), std::string::npos);
  EXPECT_NE(output.find("testFunc called with param x: 1, y:2"), std::string::npos);
  EXPECT_NE(output.find("myNativeModule has constant x: 1, constant y: y"), std::string::npos);
}

void callback(int n, int max) {
  logInfo("callback invoked with param n: " + std::to_string(n) + ", max: " + std::to_string(max) + "\n");
}

TEST_P(JSIExecutorTest, Callback) {
  testing::internal::CaptureStdout();
  auto myNativeModule = std::make_unique<test_native_module::MyNativeModule>(1);
  myNativeModule->addFunc("testCalc", testCalc, jsiexecutor::MethodType::SYNC);
  myNativeModule->addCallback("testCalc", callback);

  std::vector<std::unique_ptr<jsiexecutor::NativeModule>> nativeModules;
  nativeModules.emplace_back(std::move(myNativeModule));

  jsiexecutor::JSIExecutor jsiExecutor(std::move(runtime),
                                           std::make_shared<jsiexecutor::ModuleRegistry>(std::move(nativeModules)));
  jsiExecutor.initializeRuntime();
  std::string script = R"(
    const NativeModules = globalThis.NativeModules;
    ret = NativeModules["myNativeModule"]["testCalc"](3, 5);
    console.log("invoke called and returned: " + ret);

    ret = globalThis.invoke("myNativeModule", "testFunc", [1, 2, ([x, y]) => { console.log("callback for testFunc, x: " + x + ", y:" + y); }]);
    console.log("invoke called and returned: " + ret);
    
    ret = NativeModules["myNativeModule"]["testFunc"](1, 2,
        ([x, y]) => { console.log("callback1, x: " + x + ", y:" + y); },
        ([x, y]) => { console.log("callback2, x: " + x + ", y:" + y); },
    );
    console.log("invoke called and returned: " + ret);
  )";
  jsiExecutor.loadBundle(std::make_unique<facebook::jsi::StringBuffer>(script), "test");
  std::string output = testing::internal::GetCapturedStdout();

  EXPECT_EQ(countOccurrences(output, "calling invoke in MyNativeModule method"), 3);
  EXPECT_EQ(countOccurrences(output, "testFunc called with param x: 1, y:2"), 2);

  EXPECT_NE(output.find("testCalc called with param: n: 3, max: 5"), std::string::npos);
  EXPECT_NE(output.find("testCalc returned: 3"), std::string::npos);
  EXPECT_NE(output.find("callback invoked with param n: 3, max: 5"), std::string::npos);
}
